﻿// --------------------------------------------------------------------------------------------------------------------
// <copyright file="MonoDevAssistPackage.cs" company="Roman '4ux-nbIx' Novitsky">
//   Copyright (c) 2012 Roman '4ux-nbIx' Novitsky
// </copyright>
// <summary>
//   Defines the MonoDevAssistPackage type.
// </summary>
// --------------------------------------------------------------------------------------------------------------------

namespace MonoDevAssist
{
    #region Namespace Imports

    using System;
    using System.Collections.Generic;
    using System.ComponentModel.Design;
    using System.Globalization;
    using System.IO;
    using System.Linq;
    using System.Runtime.InteropServices;

    using EnvDTE;

    using Microsoft.VisualStudio;
    using Microsoft.VisualStudio.Shell;
    using Microsoft.VisualStudio.Shell.Interop;

    #endregion


    /// <summary>
    /// Describes and manipulates the Mono Dev Assist package.
    /// </summary>
    [ProvideProjectFactory(typeof(MonoTouch26FlavorProjectFactory), "MonoTouch Flavor", "Mono Files (*.csproj);*.csproj", "csproj", "csproj", null)]
    [ProvideProjectFactory(typeof(MonoTouch28FlavorProjectFactory), "MonoTouch Flavor", "Mono Files (*.csproj);*.csproj", "csproj", "csproj", null)]
    [PackageRegistration(UseManagedResourcesOnly = true)]
    [InstalledProductRegistration("#110", "#112", "1.0", IconResourceID = 400)]
    [ProvideMenuResource("Menus.ctmenu", 1)]
    [Guid(GuidList.MonoDevAssistPackageGuidString)]
    [ProvideAutoLoad("{f1536ef8-92ec-443c-9ed7-fdadf150da82}")]
    public sealed class MonoDevAssistPackage : Package
    {
        #region Constants and Fields

        private const string _xibBuildType = "InterfaceDefinition";
        private const string _stringsBuildType = "Content";
        private readonly MonoTouchProjectsHandler _monoTouchProjectsHandler = new MonoTouchProjectsHandler();
        private BuildEvents _buildEvents;
        private ProjectItemsEvents _csharpProjectItemsEvents;
        private DTE _dte;
        private IVsSolution _solution;
        private SolutionEvents _solutionEvents;
        private uint _solutionEventsCookie;

        #endregion


        #region Methods

        /// <summary>
        /// Initializes the package.
        /// </summary>
        protected override void Initialize()
        {
            RegisterProjectFactory(new MonoTouch26FlavorProjectFactory(this));
            RegisterProjectFactory(new MonoTouch28FlavorProjectFactory(this));

            _dte = GetService(typeof(SDTE)) as DTE;

            if (_dte == null)
            {
                Utilities.ShowMessageBox("DTE Reference Not Found");
                return;
            }

            _buildEvents = _dte.Events.BuildEvents;
            _buildEvents.OnBuildBegin += MakeXibsNone;
            _buildEvents.OnBuildDone += MakeXibsPage;

            _solution = (IVsSolution)GetService(typeof(SVsSolution));

            _csharpProjectItemsEvents = _dte.Events.GetObject("CSharpProjectItemsEvents") as ProjectItemsEvents;

            if (_csharpProjectItemsEvents == null)
            {
                Utilities.ShowMessageBox("CSharpProjectItemsEvents Reference Not Found");
                return;
            }

            _csharpProjectItemsEvents.ItemAdded += OnProjectItemAdded;

            SubscribeToSyncProjectItemEvents();

            _solutionEvents = _dte.Events.SolutionEvents;

            if (_solution == null)
            {
                Utilities.ShowMessageBox("IVSSolution Reference Not Found.");
                return;
            }

            _solution.AdviseSolutionEvents(_monoTouchProjectsHandler, out _solutionEventsCookie);

            base.Initialize();

            var menuCommandService = GetService(typeof(IMenuCommandService)) as OleMenuCommandService;

            if (menuCommandService == null)
            {
                Utilities.ShowMessageBox("IMenuCommandService Reference Not Found.");
                return;
            }

            var createCopyForMonoTouchCommandId = new CommandID(
                GuidList.MonoDevAssistCmdSetGuid, (int)PackageCommandIdList.CreateCopyForMonoTouchCommandId);

            var createCopyForMonoTouchMenuItem = new OleMenuCommand(
                OnCreateCopyForMonoTouchMenuItemClick, 
                null, 
                OnBeforeCreateCopyForMonoTouchMenuItemQuery, 
                createCopyForMonoTouchCommandId);

            menuCommandService.AddCommand(createCopyForMonoTouchMenuItem);

            var createCopyForMonoAndroidCommandId = new CommandID(
                GuidList.MonoDevAssistCmdSetGuid, (int)PackageCommandIdList.CreateCopyForMonoAndroidCommandId);

            var createCopyForMonoAndroidMenuItem = new OleMenuCommand(
                OnCreateCopyForMonoAndroidMenuItemClick, 
                null, 
                OnBeforeCreateCopyForMonoAndroidMenuItemQuery, 
                createCopyForMonoAndroidCommandId);

            menuCommandService.AddCommand(createCopyForMonoAndroidMenuItem);

            var generateControllerCommandId = new CommandID(
                GuidList.MonoDevAssistItemCommandSetGuid, (int)PackageCommandIdList.GenerateControllerCommandId);

            var generateControllerMenuItem = new OleMenuCommand(
                OnGenerateControllerMenuItemClick, 
                null, 
                OnBeforeGenerateControllerMenuItemQuery, 
                generateControllerCommandId);

            menuCommandService.AddCommand(generateControllerMenuItem);
        }


        /// <summary>
        /// Called to ask the package if the shell can be closed.
        /// </summary>
        /// <param name="canClose">
        /// If set to <c>true</c> the shell can close. 
        /// </param>
        /// <returns>
        /// Returns an instance of <see cref="System.Int32"/> . 
        /// </returns>
        protected override int QueryClose(out bool canClose)
        {
            if (_solutionEventsCookie != 0 && _solution != null)
            {
                _solution.UnadviseSolutionEvents(_solutionEventsCookie);
            }

            return base.QueryClose(out canClose);
        }


        private static void FindXibs(ProjectItems items, ICollection<ProjectItem> xibs)
        {
            foreach (ProjectItem item in items)
            {
                for (short i = 0; i < item.FileCount; ++i)
                {
                    string fileName = item.FileNames[i];
                    if (Directory.Exists(fileName))
                    {
                        FindXibs(item.ProjectItems, xibs);
                    }
                    else if (File.Exists(fileName))
                    {
                        var ext = Path.GetExtension(fileName);
                        if (!string.IsNullOrEmpty(ext) && ext.ToLower().Equals(".xib"))
                        {
                            xibs.Add(item);
                        }
                    }
                }
            }
        }


        private static IEnumerable<ProjectItem> GetSelectedProjectItems()
        {
            IntPtr hierarchyPtr;
            IntPtr selectionContainerPtr;
            IVsMultiItemSelect multiItemSelect;

            uint prjItemId;

            var monitorSelection = (IVsMonitorSelection)GetGlobalService(typeof(SVsShellMonitorSelection));

            monitorSelection.GetCurrentSelection(
                out hierarchyPtr, out prjItemId, out multiItemSelect, out selectionContainerPtr);

            IVsHierarchy selectedHierarchy;
            
            if (multiItemSelect == null)
            {
                selectedHierarchy = Marshal.GetTypedObjectForIUnknown(hierarchyPtr, typeof(IVsHierarchy)) as IVsHierarchy;
                yield return GetProjectItem(prjItemId, selectedHierarchy);
            }
            else
            {
                uint selectedItemsCount;
                int isSingleHierarchyFlag;

                multiItemSelect.GetSelectionInfo(out selectedItemsCount, out isSingleHierarchyFlag);

                var selectedItems = new VSITEMSELECTION[selectedItemsCount];
                var isSingleHierarchy = isSingleHierarchyFlag == 1;
                var flags = isSingleHierarchy ? (uint)__VSGSIFLAGS.GSI_fOmitHierPtrs : 0;

                multiItemSelect.GetSelectedItems(flags, selectedItemsCount, selectedItems);

                if (isSingleHierarchy)
                {
                    selectedHierarchy = Marshal.GetTypedObjectForIUnknown(hierarchyPtr, typeof(IVsHierarchy)) as IVsHierarchy;

                    foreach (VSITEMSELECTION selectedItem in selectedItems)
                    {
                        yield return GetProjectItem(selectedItem.itemid, selectedHierarchy);
                    }
                }
                else
                {
                    foreach (VSITEMSELECTION selectedItem in selectedItems)
                    {
                        yield return GetProjectItem(selectedItem.itemid, selectedItem.pHier);
                    }
                }
            }
        }


        private static ProjectItem GetProjectItem(uint projectItemId, IVsHierarchy selectedHierarchy)
        {
            object projectItemObject = null;

            if (selectedHierarchy != null)
            {
                ErrorHandler.ThrowOnFailure(
                    selectedHierarchy.GetProperty(projectItemId, (int)__VSHPROPID.VSHPROPID_ExtObject, out projectItemObject));
            }

            var selectedProjectItem = projectItemObject as ProjectItem;
            return selectedProjectItem;
        }


        private IEnumerable<ProjectItem> FindAllXibsInSolution()
        {
            var xibs = new List<ProjectItem>();
            foreach (Project project in _dte.Solution.Projects)
            {
                FindXibs(project, xibs);
            }

            return xibs;
        }


        private void FindXibs(Project project, List<ProjectItem> xibs)
        {
            if (project.ConfigurationManager != null)
            {
                if (project.IsMonoTouchProject())
                {
                    FindXibs(project.ProjectItems, xibs);
                }
            }
            else
            {
                NavigateProjectItems(project.ProjectItems, xibs);
            }
        }


        private void MakeXibsNone(vsBuildScope scope, vsBuildAction action)
        {
            var xibs = FindAllXibsInSolution();

            foreach (var xib in xibs)
            {
                xib.Properties.Item("ItemType").Value = "None";
            }
        }


        private void MakeXibsPage(vsBuildScope scope, vsBuildAction action)
        {
            var xibs = FindAllXibsInSolution();

            foreach (var xib in xibs)
            {
                xib.Properties.Item("ItemType").Value = _xibBuildType;
            }
        }


        private void NavigateProjectItems(ProjectItems items, List<ProjectItem> xibs)
        {
            if (items == null)
            {
                return;
            }

            items.Cast<ProjectItem>().ToList().ForEach(
                pi =>
                {
                    if (pi.SubProject != null)
                    {
                        FindXibs(pi.SubProject, xibs);
                    }
                });
        }


        private void OnBeforeCreateCopyForMonoAndroidMenuItemQuery(object sender, EventArgs e)
        {
            OnBeforeCreateCopyMenuItemQuery(sender, "Android", ProjectExtensions.IsMonoAndroidLibraryProject);
        }


        private void OnBeforeCreateCopyForMonoTouchMenuItemQuery(object sender, EventArgs e)
        {
            OnBeforeCreateCopyMenuItemQuery(sender, "iOS", ProjectExtensions.IsMonoTouchProject);
        }


        private void OnBeforeCreateCopyMenuItemQuery(
            object sender, string platformName, Func<Project, bool> isTargetPlatformProject)
        {
            var command = sender as OleMenuCommand;

            if (command == null)
            {
                return;
            }

            Project activeProject = _dte.GetActiveProject();

            if (activeProject == null)
            {
                command.Visible = false;
                return;
            }

            command.Text = string.Format(
                CultureInfo.InvariantCulture, 
                "Create Copy of {0} for {1}", 
                activeProject.GetDefaultNamespace(), 
                platformName);
            command.Visible = true;

            if (isTargetPlatformProject(activeProject))
            {
                command.Enabled = false;
                return;
            }

            List<Project> projectCopies = activeProject.GetProjectCopies();

            if (projectCopies.Any(isTargetPlatformProject))
            {
                command.Enabled = false;
                return;
            }

            command.Enabled = true;
        }


        private void OnBeforeGenerateControllerMenuItemQuery(object sender, EventArgs e)
        {
            var command = sender as OleMenuCommand;

            if (command == null)
            {
                return;
            }

            var selectedProjectItems = GetSelectedXibs().ToList();

            if (selectedProjectItems.Count == 0)
            {
                command.Visible = false;
                return;
            }

            command.Text = selectedProjectItems.Count > 1 ? "Generate Controllers" : "Generate Controller";
            command.Visible = true;
            command.Enabled = true;
        }


        private IEnumerable<ProjectItem> GetSelectedXibs()
        {
            var selectedProjectItems = GetSelectedProjectItems();

            foreach (ProjectItem projectItem in selectedProjectItems)
            {
                var fileName = projectItem.FileNames[1];

                var extension = Path.GetExtension(fileName);

                if (string.IsNullOrEmpty(extension) || extension.ToUpperInvariant() != ".XIB")
                {
                    continue;
                }

                yield return projectItem;
            }
        }


        private void OnCreateCopyForMonoAndroidMenuItemClick(object sender, EventArgs e)
        {
            OnCreateCopyMenuItemClick("MonoDevAssist.Resources.MonoAndroid.csproj", "Android");
        }


        private void OnCreateCopyForMonoTouchMenuItemClick(object sender, EventArgs e)
        {
            OnCreateCopyMenuItemClick("MonoDevAssist.Resources.MonoTouch.csproj", "iOS");
        }


        private void OnCreateCopyMenuItemClick(string projectTemplatePath, string platformName)
        {
            Stream stream = typeof(MonoDevAssistPackage).Assembly.GetManifestResourceStream(projectTemplatePath);

            if (stream == null)
            {
                Utilities.ShowMessageBox("Unexpected error occurred.");

                return;
            }

            string projectTemplate;

            try
            {
                using (var reader = new StreamReader(stream))
                {
                    stream = null;
                    projectTemplate = reader.ReadToEnd();
                }
            }
            finally
            {
                if (stream != null)
                {
                    stream.Close();
                }
            }

            Project activeProject = _dte.GetActiveProject();
            string directoryName = Path.GetDirectoryName(activeProject.FileName);

            if (string.IsNullOrEmpty(directoryName))
            {
                return;
            }

            string defaultNamespace = activeProject.GetDefaultNamespace();

            projectTemplate = projectTemplate.Replace(
                "<RootNamespace></RootNamespace>", "<RootNamespace>" + defaultNamespace + "</RootNamespace>");
            projectTemplate = projectTemplate.Replace(
                "<AssemblyName></AssemblyName>", "<AssemblyName>" + defaultNamespace + "</AssemblyName>");
            projectTemplate = projectTemplate.Replace(
                "<ProjectGuid></ProjectGuid>", "<ProjectGuid>" + Guid.NewGuid() + "</ProjectGuid>");

            string fileName = string.Format(
                CultureInfo.InvariantCulture, "{0} ({1}).csproj", defaultNamespace, platformName);

            fileName = Path.Combine(directoryName, fileName);

            try
            {
                stream = new FileStream(fileName, FileMode.Create);

                using (var writer = new StreamWriter(stream))
                {
                    stream = null;
                    writer.Write(projectTemplate);
                }
            }
            finally
            {
                if (stream != null)
                {
                    stream.Close();
                }
            }

            _solutionEvents.ProjectAdded += OnProjectAdded;

            _dte.Solution.AddFromFile(fileName);
        }


        private void OnGenerateControllerMenuItemClick(object sender, EventArgs e)
        {
            var projectItems = GetSelectedXibs();

            var generator = new XibControllerGenerator();

            try
            {
                generator.GenerateController(projectItems.Select(i => i.FileNames[1]), _dte);
            }
            catch (Exception exception)
            {
                Utilities.ShowMessageBox("Failed to generate controller: " + exception.Message);
            }
        }


        private void OnProjectAdded(Project project)
        {
            Project sourceProject = project.GetProjectCopies().First();

            SyncFolderItems(project, sourceProject.ProjectItems);

            _solutionEvents.ProjectAdded -= OnProjectAdded;
        }


        private void OnProjectItemAdded(ProjectItem projectItem)
        {
            if (!projectItem.ContainingProject.IsMonoTouchProject())
            {
                return;
            }

            var extension = Path.GetExtension(projectItem.FileNames[1]);

            if (string.IsNullOrEmpty(extension))
            {
                return;
            }

            if (extension.ToUpperInvariant() == ".STRINGS")
            {
                projectItem.Properties.Item("ItemType").Value = _stringsBuildType;
            }
            else if (extension.ToUpperInvariant() == ".XIB")
            {
                projectItem.Properties.Item("ItemType").Value = _xibBuildType;
            }
        }


        private void SubscribeToSyncProjectItemEvents()
        {
            _csharpProjectItemsEvents.ItemAdded += SyncAddedProjectItem;
            _csharpProjectItemsEvents.ItemRemoved += SyncRemovedProjectItem;
            _csharpProjectItemsEvents.ItemRenamed += SyncRenamedProjectItem;
        }


        private void SyncAddedProjectItem(ProjectItem projectItem)
        {
            if (projectItem.IsFolder())
            {
                return;
            }

            UnsubscribeFromSyncProjectItemEvents();

            try
            {
                var projectCopies = projectItem.ContainingProject.GetProjectCopies();

                foreach (Project projectCopy in projectCopies)
                {
                    projectCopy.ProjectItems.AddFromFile(projectItem.FileNames[1]);
                }
            }
            finally
            {
                SubscribeToSyncProjectItemEvents();
            }
        }


        private void SyncFolderItems(Project project, ProjectItems folderItems)
        {
            for (int i = 1; i <= folderItems.Count; i++)
            {
                ProjectItem item = folderItems.Item(i);

                ProjectItem itemCopy = project.FindItemCopy(item);

                if (itemCopy != null)
                {
                    continue;
                }

                if (item.IsFolder())
                {
                    SyncFolderItems(project, item.ProjectItems);
                }
                else if (File.Exists(item.FileNames[1]))
                {
                    project.ProjectItems.AddFromFile(item.FileNames[1]);
                }
            }
        }


        private void SyncRemovedProjectItem(ProjectItem projectItem)
        {
            UnsubscribeFromSyncProjectItemEvents();

            try
            {
                var projectCopies = projectItem.ContainingProject.GetProjectCopies();

                foreach (Project projectCopy in projectCopies)
                {
                    ProjectItem item = projectCopy.FindItemCopy(projectItem);

                    if (item != null)
                    {
                        item.Remove();
                    }
                }
            }
            finally
            {
                SubscribeToSyncProjectItemEvents();
            }
        }


        private void SyncRenamedProjectItem(ProjectItem projectItem, string oldName)
        {
            UnsubscribeFromSyncProjectItemEvents();

            try
            {
                var projectCopies = projectItem.ContainingProject.GetProjectCopies();

                foreach (Project projectCopy in projectCopies)
                {
                    string fileName = projectItem.FileNames[1];

                    fileName = fileName.TrimEnd(new[] { '\\' });

                    string directoryName = Path.GetDirectoryName(fileName);

                    if (string.IsNullOrEmpty(directoryName))
                    {
                        continue;
                    }

                    string oldFileName = Path.Combine(directoryName, oldName);

                    if (projectItem.IsFolder())
                    {
                        oldFileName += "\\";
                    }

                    ProjectItem item = projectCopy.FindItem(oldFileName);

                    if (item == null)
                    {
                        continue;
                    }

                    item.Remove();

                    if (projectItem.IsFolder())
                    {
                        SyncFolderItems(projectCopy, projectItem.ProjectItems);
                    }
                    else
                    {
                        projectCopy.ProjectItems.AddFromFile(projectItem.FileNames[1]);
                    }
                }
            }
            finally
            {
                SubscribeToSyncProjectItemEvents();
            }
        }


        private void UnsubscribeFromSyncProjectItemEvents()
        {
            _csharpProjectItemsEvents.ItemAdded -= SyncAddedProjectItem;
            _csharpProjectItemsEvents.ItemRemoved -= SyncRemovedProjectItem;
            _csharpProjectItemsEvents.ItemRenamed -= SyncRenamedProjectItem;
        }

        #endregion
    }
}